package hms.UserAccountsModule;

import hms.database.DAO;
import hms.database.DAOnoGUI;
import hms.database.DBRowObject;
import hms.employees.Employee;
import hms.permissions.*;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * A UserAccount represents a user account in the system through which an end
 * user can authenticate.
 * 
 * @author Ben Adcock
 * 
 */

/**
 * @author Ben
 *
 */
public class UserAccount extends DBRowObject {
	
	// Username
	private String username;
	// Program permissions
	private PermissionSet permissions;
	private String passwordHash;
	private Employee employeeRecord;

	
	/**
	 * Create a UserAccount with default nonsense parameters.  This UserAccount will not authenticate
	 * until the username and password are set.
	 */
	public UserAccount() {
		super();
		username = "no_username";
		permissions = new PermissionSet();
		passwordHash = "NOPASSWORDSET";
		employeeRecord = new Employee();
	}
	
	/**
	 * Load a UserAccount from the database using the primary key uid.
	 * 
	 * @param uid
	 */
	public UserAccount(int uid) {
		super(uid);
	}
	
	public UserAccount(String username, String password) {
		this(username,password,new PermissionSet());
	}

	public UserAccount(String username, String password, PermissionSet ps) {
		this();
		setUsername(username);
		setPassword(password);
		setPermissions(ps);
	}

	/**
	 * Gets username of user.
	 * 
	 * @return user name
	 */
	public String getUserName() {
		return username;
	}
	
	/**
	 * Return the user's username, hash of password and full name (if linked to a Employee)
	 * 
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		if(employeeRecord.isDefault()) {
			return username;
		} else {
			return employeeRecord.getFullName() + " (" + username + ")";
		}
	}
	
	public String getStatusString() {
		if(employeeRecord.isDefault()) {
			return "No linked Employee account!";
		} else if(employeeRecord.isTerminated()) {
			return "Fired " + employeeRecord.getFireMonth() + "/" + employeeRecord.getFireDay() + "/" + employeeRecord.getFireYear();
		} else {
			return employeeRecord.getPosition() + " - Hired " + employeeRecord.getHireMonth() + "/" + employeeRecord.getHireDay() + "/" + employeeRecord.getHireYear();
		}
	}
	
	/**
	 * Gets full name of employee, if an Employee is linked to this UserAccount;
	 * otherwise, return the username.
	 * 
	 * @return Employee's full name
	 */
	public String getFullEmployeeName() {
		if(employeeRecord.isDefault()) {
			return username;
		} else {
			return employeeRecord.getFullName();
		}
	}

	/**
	 * Gets PermissionSet of permissions.
	 * 
	 * @return PermissionSet of permissions
	 */
	public PermissionSet getPermissions() {
		return permissions;
	}

	/**
	 * setPermissions allows you to change a user's permission set
	 * by uploading a new one.
	 * <p>
	 * <b>Postcondition:</b> This object will be marked as changed from the DB copy.
	 * 
	 * @param ps New PermissionSet
	 */
	public void setPermissions(PermissionSet ps) {
		permissions = ps;
		markAsChanged();
	}
	
	public void grantPermission(Permission p) {
		permissions.grantPermission(p);
	}
	
	public void denyPermission(Permission p) {
		permissions.denyPermission(p);
	}

	/**
	 * setUsername sets the username of the currently loaded user to the input
	 * string.
	 * <p>
	 * <b>Postcondition:</b> This object will be marked as changed from the DB copy.
	 * 
	 * @param newUsername
	 *            the new username
	 */
	public void setUsername(String newUsername) {
		username = newUsername.replaceAll("[^A-Za-z0-9]+", "");
		markAsChanged();
	}

	/**
	 * Hash the input password string and save it.
	 * <p>
	 * <b>Postcondition:</b> This object will be marked as changed from the DB copy.
	 * 
	 * @param newPassword
	 */
	public void setPassword(String newPassword) {
		passwordHash = hashPassword(newPassword);
		markAsChanged();
	}
	
	/**
	 * Link this UserAccount to a defined Employee account.  The default place holder
	 * Employee record will be deleted.  Both the Employee being linked and the
	 * UserAccount will be committed to the database before linking.
	 * <p>
	 * <b>Postconditions:</b>
	 * <ul>
	 * <li>This object is saved to the database.
	 * <li>The input Employee is saved to the database.
	 * <li>If the current linked Employee is the default, it is deleted.
	 * <li>This UserAccount will be linked to the Employee account.
	 * </ul>
	 * 
	 * @param employee
	 * 				An Employee to link with this UserAccount.
	 */
	public void linkToEmployee(Employee employee) {
		// Ensure unique IDs are valid
		this.commitToDatabase();
		employee.commitToDatabase();
		
		// Update database
		DAO localDAO = DAOnoGUI.getDAO();
		localDAO.updateCommand("UPDATE USERACCOUNT SET EMPLOYEE_ID="+employee.getUniqueIdentifier()+" WHERE USER_ID="+getUniqueIdentifier()+";");
		DAOnoGUI.closeDAO();

		// Remove old if old was default
		if(employeeRecord.isDefault())
		{
			employeeRecord.markToBeDeleted();
			employeeRecord.commitToDatabase();
		}
		
		employeeRecord = employee;
	}
	
	public boolean isLinked() {
		return !employeeRecord.isDefault();
	}
	
	public void unlinkEmployee() {
		employeeRecord = new Employee();
		markAsChanged();
	}
	
	/**
	 * Returns the Employee record linked with this user account.
	 * 
	 * @return Employee
	 */
	public Employee getEmployee() {
		return employeeRecord;
	}
	
	/**
	 * Performs an salt and MD5 hash of the input password.
	 * 
	 * @param password String representing password to be hashed.
	 * @return String representing hash, in hex format.
	 */
	private String hashPassword(String password) {
		byte[] message;
		MessageDigest md;
		byte[] theDigest = null;
		
		try {
			// salt
			message = (password + "Free Bird, by Lynyrd Skynyrd").getBytes("UTF-8");
			md = MessageDigest.getInstance("MD5");
			theDigest = md.digest(message);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return convertBytesToHexString(theDigest);
	}
	
	/**
	 * Small function to convert a byte array to a String representing the input in hex.
	 * 
	 * @param bytes A byte array
	 * @return A String representing the input array with hex characters
	 */
	private String convertBytesToHexString(byte[] bytes) {

		String num2hex = "0123456789ABCDEF";

		StringBuffer sb = new StringBuffer();
		for(int i=0;i<bytes.length;i++) {
			int high = (int) (Math.floor((bytes[i]%256)/16));
			if(high<0) { high += 16; }
			sb.append(num2hex.charAt(high));
			int low = (bytes[i]%16);
			if(low<0) { low += 16; }
			sb.append(num2hex.charAt(low));

		}
		return sb.toString();

	}
	
	
	/**
	 * Check password runs the input string against the database for the current
	 * user.  Passwords are hashed before checking against database.
	 * 
	 * @param password  String containing password to be checked.
	 * @return          True if success, false otherwise
	 */
	public boolean checkPassword(String password) {
		String temphash = hashPassword(password);
		boolean isSame = passwordHash.equals(temphash);
		return isSame;
	}

	
	/**
	 * Returns true if specific permission is granted OR if FULL_ACCESS is granted
	 * 
	 * @param p
	 * @return
	 */
	public boolean isPermissionGranted(Permission p) {
		return permissions.isPermissionGranted(p)||permissions.isPermissionGranted(Permission.FULL_ACCESS);
	}
	
	/**
	 * Returns true if and only if specific permission is granted
	 * 
	 * @param p
	 * @return
	 */
	public boolean isSpecificPermissionGranted(Permission p) {
		return permissions.isPermissionGranted(p);
	}

	@Override
	protected String[] getFieldNames() {
		String[] out = new String[4];
		out[0] = "USERNAME";
		out[1] = "PASSWORDHASH";
		out[2] = "PERMISSIONS";
		out[3] = "EMPLOYEE_ID";
		return out;
	}

	@Override
	protected void loadPreparedStatement(PreparedStatement stmt)
			throws SQLException {
		stmt.setString(1, username);
		stmt.setString(2, passwordHash);
		stmt.setString(3, permissions.getDBString());
		employeeRecord.commitToDatabase();
		stmt.setInt(4, employeeRecord.getUniqueIdentifier());
	}

	@Override
	protected String getUniqueIdentifierFieldName() {
		return "USER_ID";
	}

	@Override
	protected String getTableName() {
		return "USERACCOUNT";
	}

	@Override
	protected void loadFromDatabase(ResultSet rs) throws SQLException {
		this.username = rs.getString("USERNAME");
		this.passwordHash = rs.getString("PASSWORDHASH");
		this.permissions = new PermissionSet(rs.getString("PERMISSIONS"));
		this.employeeRecord = new Employee(rs.getInt("EMPLOYEE_ID"));
	}

	@Override
	protected boolean allowDuplicateRows() {
		return false;
	}
	
    @Override
	public void commitToDatabase() {
		employeeRecord.commitToDatabase();
		super.commitToDatabase();
	}

}
